﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Policy;

namespace Utilities
{
    public interface IAssemblyProcessor
    {
        /// <summary>
        /// The path where the assembly to be preocessed is located
        /// </summary>
        string LibraryDirectoryPath { get; set; }

        /// <summary>
        /// Processes the assembly
        /// </summary>
        /// <param name="assembly"></param>
        IList<LogMessage> Process(Assembly assembly);  
    }

    /// <summary>
    /// Encapsulates an AppDomain to load and unload assemblies
    /// </summary>
    public class AssemblyModule : IDisposable
    {
        public AssemblyModule(string name = null)
        {
            // Create a unique name if none is provided
            if (string.IsNullOrWhiteSpace(name))
            {
                name = Guid.NewGuid().ToString();
            }

            // Create the child app domain
            AppDomain parentDomain = AppDomain.CurrentDomain;

            Evidence evidence = new Evidence(parentDomain.Evidence);

            AppDomainSetup domainSetup = new AppDomainSetup
            {
                ApplicationBase = string.IsNullOrWhiteSpace(parentDomain.RelativeSearchPath) ? parentDomain.BaseDirectory : parentDomain.RelativeSearchPath,
                DisallowBindingRedirects = false,
                DisallowCodeDownload = true,
                ConfigurationFile = parentDomain.SetupInformation.ConfigurationFile
            };

            _childDomain = AppDomain.CreateDomain(name, evidence, domainSetup);

            // Create the proxy to return the assembly reference from this app domain
            Type type = typeof(AssemblyLoader);
            _loader = (AssemblyLoader)_childDomain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
        }

        public IList<LogMessage> Load(string path, IAssemblyProcessor processor, out bool failed, bool forReflectionOnly = false)
        {
            PreCondition.RequireNotNullOrEmpty(path, "path");
            PreCondition.RequireNotNull(processor, "processor");

            string directoryPath = new FileInfo(path).Directory.FullName;
            PreCondition.RequireTrue(Directory.Exists(directoryPath), string.Format("Directory not found: '{0}'", directoryPath));

            processor.LibraryDirectoryPath = directoryPath;

            return _loader.Load(path, processor, out failed, forReflectionOnly: forReflectionOnly);
        }

        public void Dispose()
        {
            AppDomain.Unload(_childDomain);
        }

        public class AssemblyLoader : MarshalByRefObject
        {
            internal IList<LogMessage> Load(string path, IAssemblyProcessor processor, out bool failed, bool forReflectionOnly)
            {
                List<LogMessage> messages = new List<LogMessage>();
                failed = false;

                Assembly assembly;

                if (forReflectionOnly)
                {
                    ResolveEventHandler resolveEventHandler = (s, args) =>
                    {
                        return ReflectionOnlyResolve(path, args);
                    };

                    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;

                    assembly = Assembly.ReflectionOnlyLoadFrom(path);

                    failed = !ProcessAssembly(assembly, processor, messages);

                    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
                }
                else
                {
                    assembly = Assembly.LoadFrom(path);

                    failed = !ProcessAssembly(assembly, processor, messages);
                }

                return messages;
            }

            private bool ProcessAssembly(Assembly assembly, IAssemblyProcessor processor, List<LogMessage> messages)
            {
                if (VerifyReferencedAssembliesExist(assembly, processor, messages))
                {
                    IList<LogMessage> msgs = processor.Process(assembly);
                    messages.AddRange(msgs);

                    if (messages.Where(m => m.Severity == LogMessageFactory.Severities.Error).Any())
                    {
                        return false; // Error messages mean failure
                    }

                    return true; // Success
                }

                return false; // Missing referenced libraries (failure)
            }

            #region Helpers

            private Assembly ReflectionOnlyResolve(string path, ResolveEventArgs args)
            {
                Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().
                    FirstOrDefault(asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));

                if (loadedAssembly != null)
                {
                    return loadedAssembly;
                }

                AssemblyName assemblyName = new AssemblyName(args.Name);
                string directoryPath = Path.GetDirectoryName(path);
                string dependencyPath = Path.Combine(directoryPath, assemblyName.Name + ".dll");

                if (File.Exists(dependencyPath))
                {
                    return Assembly.ReflectionOnlyLoadFrom(dependencyPath);
                }

                return Assembly.ReflectionOnlyLoad(args.Name);
            }

            /// <summary>
            /// // Verifies that all the referenced assemblies exist in the processor directory
            /// </summary>
            /// <param name="assembly"></param>
            /// <returns></returns>
            private bool VerifyReferencedAssembliesExist(Assembly assembly, IAssemblyProcessor processor, IList<LogMessage> messages)
            {
                bool filesExist = true;

                foreach (AssemblyName assemblyName in assembly.GetReferencedAssemblies())
                {
                    if (assemblyName.GetPublicKeyToken().Length == 0) // We assume that signed libraries are in GAC, therefore if not in GAC it needs to be in the directory
                    {
                        string fileName = assemblyName.Name + ".dll";
                        string path = Path.Combine(processor.LibraryDirectoryPath, fileName);

                        if (!File.Exists(path))
                        {
                            filesExist = false;
                            AddMissingReferencedLibraryMessage(messages, fileName);
                        }
                    }
                }

                return filesExist;
            }

            private void AddMissingReferencedLibraryMessage(IList<LogMessage> messages, string fileName)
            {
                LogMessage message = new LogMessageFactory
                {
                    Severity = LogMessageFactory.Severities.Error,
                    Key = "MissingReferencedLibrary",
                    Text = "Missing referenced assembly: '$fileName$'",
                    Values = 
                    {
                        { "fileName", fileName }
                    }
                }
                .Create();

                messages.Add(message);
            }

            #endregion
        }

        private AppDomain _childDomain;

        private AssemblyLoader _loader;

    }
}
